Django REST Framework

Konfigurieren von URLs und Routing

Von einfachen URL-Patterns zu automatischem Routing mit Routers

📋 Agenda

1️⃣ URL Basics

  • Django URL-Patterns Recap
  • DRF URL-Struktur
  • path() vs re_path()

2️⃣ Manuelle URLs

  • APIView URLs
  • Generic Views URLs
  • URL-Parameter

3️⃣ Routers

  • SimpleRouter
  • DefaultRouter
  • Custom Actions

4️⃣ Advanced

  • Nested Routing
  • Versioning
  • Best Practices

🔄 Django URL Basics - Recap

Wie funktionieren URLs in Django?

Request → URL Pattern → View → Response

Standard Django URL Pattern:

# filepath: movieapi/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('movies/', include('movies.urls')),  # ← App URLs einbinden
]

App URLs:

# filepath: movies/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('', views.movie_list, name='movie-list'),           # /movies/
    path('<int:pk>/', views.movie_detail, name='movie-detail'),  # /movies/1/
]

🎯 URL Pattern Komponenten:

  • path(''): Route/Pfad
  • views.movie_list: View-Funktion oder Klasse
  • name='movie-list': URL-Name für reverse()
  • <int:pk>: URL-Parameter (Converter)

🏗️ DRF URL-Struktur

3 Ansätze für DRF URLs

1. Manuelle URLs (APIView)

# filepath: movies/urls.py
from django.urls import path
from .views import MovieListAPIView, MovieDetailAPIView

urlpatterns = [
    path('movies/', MovieListAPIView.as_view()),
    path('movies/<int:pk>/', MovieDetailAPIView.as_view()),
]

✅ Maximale Kontrolle

❌ Viel Code

2. Manuelle URLs (Generic Views)

# filepath: movies/urls.py
from django.urls import path
from .views import MovieListCreateView, MovieDetailView

urlpatterns = [
    path('movies/', MovieListCreateView.as_view()),
    path('movies/<int:pk>/', MovieDetailView.as_view()),
]

✅ Weniger Code

❌ Immer noch manuell

3. Router (ViewSets)

# filepath: movies/urls.py
from rest_framework.routers import DefaultRouter
from .views import MovieViewSet

router = DefaultRouter()
router.register(r'movies', MovieViewSet)

urlpatterns = router.urls

✅ Automatische URLs

✅ RESTful Standard

⭐ Empfohlen!

🔀 path() vs re_path()

1. path() - Einfache Patterns (Django 2.0+)

from django.urls import path

urlpatterns = [
    # Integer Parameter
    path('movies/<int:pk>/', views.movie_detail),
    # /movies/1/
    
    # String Parameter
    path('movies/<str:slug>/', views.movie_by_slug),
    # /movies/the-matrix/
    
    # UUID Parameter
    path('movies/<uuid:id>/', views.movie_by_uuid),
    # /movies/550e8400-e29b-41d4-a716-446655440000/
    
    # Path Parameter (mehrere Segmente)
    path('files/<path:filepath>/', views.file_detail),
    # /files/docs/2023/report.pdf
]

✅ Vorteile: Einfach, lesbar, typsicher

2. re_path() - RegEx Patterns

from django.urls import re_path

urlpatterns = [
    # Jahr (4 Ziffern)
    re_path(r'^movies/(?P<year>[0-9]{4})/$', views.movies_by_year),
    # /movies/2023/
    
    # Slug (Buchstaben, Zahlen, Bindestriche)
    re_path(r'^movies/(?P<slug>[-\w]+)/$', views.movie_by_slug),
    # /movies/the-matrix-1999/
    
    # Custom Pattern
    re_path(r'^api/v(?P<version>[0-9]+)/movies/$', views.movie_list),
    # /api/v1/movies/, /api/v2/movies/
]

✅ Vorteile: Maximale Flexibilität

❌ Nachteile: Komplexer, fehleranfällig

🎯 Wann was nutzen?

  • path(): 95% der Fälle (Standard)
  • re_path(): Nur bei komplexen Pattern-Anforderungen

📝 APIView URLs - Manuell

Jede URL einzeln definieren

Views definieren:

# filepath: movies/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .models import Movie
from .serializers import MovieSerializer


class MovieListAPIView(APIView):
    """GET: Liste, POST: Erstellen"""
    
    def get(self, request):
        movies = Movie.objects.all()
        serializer = MovieSerializer(movies, many=True)
        return Response(serializer.data)
    
    def post(self, request):
        serializer = MovieSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


class MovieDetailAPIView(APIView):
    """GET: Detail, PUT: Update, DELETE: Löschen"""
    
    def get(self, request, pk):
        movie = get_object_or_404(Movie, pk=pk)
        serializer = MovieSerializer(movie)
        return Response(serializer.data)
    
    def put(self, request, pk):
        movie = get_object_or_404(Movie, pk=pk)
        serializer = MovieSerializer(movie, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
    
    def delete(self, request, pk):
        movie = get_object_or_404(Movie, pk=pk)
        movie.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

URLs konfigurieren:

# filepath: movies/urls.py
from django.urls import path
from .views import MovieListAPIView, MovieDetailAPIView

urlpatterns = [
    path('movies/', MovieListAPIView.as_view(), name='movie-list'),
    path('movies/<int:pk>/', MovieDetailAPIView.as_view(), name='movie-detail'),
]

Haupt-URLs:

# filepath: movieapi/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('movies.urls')),  # ← Präfix 'api/'
]

📍 Resultierende Endpoints:

GET    /api/movies/      → Liste aller Filme
POST   /api/movies/      → Film erstellen
GET    /api/movies/1/    → Film #1 Details
PUT    /api/movies/1/    → Film #1 aktualisieren
DELETE /api/movies/1/    → Film #1 löschen

🔨 Generic Views URLs

Weniger Views, gleiche URLs

Views definieren:

# filepath: movies/views.py
from rest_framework import generics
from .models import Movie
from .serializers import MovieSerializer


class MovieListCreateView(generics.ListCreateAPIView):
    """GET: Liste, POST: Erstellen"""
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer


class MovieDetailView(generics.RetrieveUpdateDestroyAPIView):
    """GET: Detail, PUT/PATCH: Update, DELETE: Löschen"""
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer

URLs konfigurieren:

# filepath: movies/urls.py
from django.urls import path
from .views import MovieListCreateView, MovieDetailView

urlpatterns = [
    path('movies/', MovieListCreateView.as_view(), name='movie-list'),
    path('movies/<int:pk>/', MovieDetailView.as_view(), name='movie-detail'),
]

❌ APIView

2 Views × ~30 Zeilen = 60 Zeilen

2 URL-Patterns

✅ Generic Views

2 Views × 3 Zeilen = 6 Zeilen

2 URL-Patterns (gleich)

📍 Resultierende Endpoints:

GET    /api/movies/      → Liste aller Filme
POST   /api/movies/      → Film erstellen
GET    /api/movies/1/    → Film #1 Details
PUT    /api/movies/1/    → Film #1 komplett aktualisieren
PATCH  /api/movies/1/    → Film #1 teilweise aktualisieren
DELETE /api/movies/1/    → Film #1 löschen

🎯 ViewSets - Der moderne Ansatz

1 ViewSet = Alle CRUD-Operationen

URLs werden automatisch mit Routers generiert

ViewSet definieren:

# filepath: movies/views.py
from rest_framework import viewsets
from .models import Movie
from .serializers import MovieSerializer


class MovieViewSet(viewsets.ModelViewSet):
    """
    ViewSet für Movie CRUD-Operationen.
    
    Automatisch bereitgestellt:
    - list(): GET /movies/
    - create(): POST /movies/
    - retrieve(): GET /movies/{pk}/
    - update(): PUT /movies/{pk}/
    - partial_update(): PATCH /movies/{pk}/
    - destroy(): DELETE /movies/{pk}/
    """
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer

Das war's! Nur 3 Zeilen Code! 🚀

💡 Aber wie werden die URLs erstellt?

→ Mit einem Router!

🔧 SimpleRouter

Automatisches URL-Routing für ViewSets

SimpleRouter Setup:

# filepath: movies/urls.py
from rest_framework.routers import SimpleRouter
from .views import MovieViewSet

# Router erstellen
router = SimpleRouter()

# ViewSet registrieren
router.register(r'movies', MovieViewSet)

# URLs exportieren
urlpatterns = router.urls

Haupt-URLs:

# filepath: movieapi/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('movies.urls')),
]

📍 Automatisch generierte Endpoints:

GET    /api/movies/          → movie-list
POST   /api/movies/          → movie-list
GET    /api/movies/{pk}/     → movie-detail
PUT    /api/movies/{pk}/     → movie-detail
PATCH  /api/movies/{pk}/     → movie-detail
DELETE /api/movies/{pk}/     → movie-detail

Eigenschaften von SimpleRouter:

  • ✅ Minimalistisch
  • ✅ Standard REST-Endpoints
  • ❌ KEINE Root-API-View
  • ❌ Keine trailing slashes ohne Konfiguration

⭐ DefaultRouter - Empfohlen!

SimpleRouter + Root API View

DefaultRouter Setup:

# filepath: movies/urls.py
from rest_framework.routers import DefaultRouter
from .views import MovieViewSet, ArtistViewSet

# Router erstellen
router = DefaultRouter()

# ViewSets registrieren
router.register(r'movies', MovieViewSet, basename='movie')
router.register(r'artists', ArtistViewSet, basename='artist')

# URLs exportieren
urlpatterns = router.urls

📍 Automatisch generierte Endpoints:

# Root API (Übersicht)
GET    /api/                 → api-root

# Movies
GET    /api/movies/          → movie-list
POST   /api/movies/          → movie-list
GET    /api/movies/{pk}/     → movie-detail
PUT    /api/movies/{pk}/     → movie-detail
PATCH  /api/movies/{pk}/     → movie-detail
DELETE /api/movies/{pk}/     → movie-detail

# Artists
GET    /api/artists/         → artist-list
POST   /api/artists/         → artist-list
GET    /api/artists/{pk}/    → artist-detail
PUT    /api/artists/{pk}/    → artist-detail
PATCH  /api/artists/{pk}/    → artist-detail
DELETE /api/artists/{pk}/    → artist-detail

Root API View - Beispiel Response:

GET /api/

{
    "movies": "http://localhost:8000/api/movies/",
    "artists": "http://localhost:8000/api/artists/"
}

SimpleRouter

❌ Keine Root View

✅ Minimalistisch

DefaultRouter

✅ Root API View

✅ Besser für Browsable API

⭐ Empfohlen!

🏷️ Router Parameter - basename

Wozu dient basename?

Automatischer basename (Standard):

# filepath: movies/urls.py
router = DefaultRouter()
router.register(r'movies', MovieViewSet)
# ↑ basename wird automatisch aus Model generiert: 'movie'

URL-Namen: movie-list, movie-detail

Manueller basename (erforderlich wenn kein queryset):

# filepath: movies/views.py
class MovieViewSet(viewsets.ModelViewSet):
    serializer_class = MovieSerializer
    
    def get_queryset(self):
        """Dynamisches QuerySet"""
        user = self.request.user
        if user.is_staff:
            return Movie.objects.all()
        return Movie.objects.filter(created_by=user)
    
    # Kein queryset-Attribut!


# filepath: movies/urls.py
router = DefaultRouter()
router.register(r'movies', MovieViewSet, basename='movie')
#                                        ^^^^^^^^^^^^^^^^
# basename MUSS angegeben werden!

Custom basename:

router = DefaultRouter()
router.register(r'my-movies', MovieViewSet, basename='my-movie')
# URL-Namen: 'my-movie-list', 'my-movie-detail'

🎯 basename Verwendung:

  • URL reverse(): reverse('movie-list')
  • In Templates: {% url 'movie-detail' pk=1 %}
  • Serializer Hyperlinks: HyperlinkedModelSerializer

🎬 Custom Actions in ViewSets

Nicht-CRUD Operationen hinzufügen

Router generiert automatisch URLs für @action

ViewSet mit Custom Actions:

# filepath: movies/views.py
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from django.db.models import Avg


class MovieViewSet(viewsets.ModelViewSet):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer
    
    @action(detail=False, methods=['get'])
    def top_rated(self, request):
        """
        Top 10 Filme nach Rating
        URL: GET /api/movies/top_rated/
        """
        top_movies = Movie.objects.order_by('-rating')[:10]
        serializer = self.get_serializer(top_movies, many=True)
        return Response(serializer.data)
    
    @action(detail=False, methods=['get'])
    def recent(self, request):
        """
        Filme der letzten 5 Jahre
        URL: GET /api/movies/recent/
        """
        current_year = datetime.now().year
        recent_movies = Movie.objects.filter(year__gte=current_year - 5)
        serializer = self.get_serializer(recent_movies, many=True)
        return Response(serializer.data)
    
    @action(detail=True, methods=['post'])
    def rate(self, request, pk=None):
        """
        Film bewerten
        URL: POST /api/movies/{pk}/rate/
        """
        movie = self.get_object()
        rating = request.data.get('rating')
        
        # Validierung
        if not rating or not (0 <= float(rating) <= 10):
            return Response(
                {'error': 'Rating must be between 0 and 10'},
                status=400
            )
        
        movie.rating = rating
        movie.save()
        
        serializer = self.get_serializer(movie)
        return Response(serializer.data)
    
    @action(detail=True, methods=['get'])
    def cast(self, request, pk=None):
        """
        Besetzung eines Films
        URL: GET /api/movies/{pk}/cast/
        """
        movie = self.get_object()
        castings = movie.castings.all()
        serializer = MovieCastingSerializer(castings, many=True)
        return Response(serializer.data)

URLs Setup (unverändert!):

# filepath: movies/urls.py
from rest_framework.routers import DefaultRouter
from .views import MovieViewSet

router = DefaultRouter()
router.register(r'movies', MovieViewSet)

urlpatterns = router.urls

📍 Automatisch generierte Endpoints:

# Standard CRUD
GET    /api/movies/              → movie-list
POST   /api/movies/              → movie-list
GET    /api/movies/{pk}/         → movie-detail
PUT    /api/movies/{pk}/         → movie-detail
PATCH  /api/movies/{pk}/         → movie-detail
DELETE /api/movies/{pk}/         → movie-detail

# Custom Actions (Collection)
GET    /api/movies/top_rated/    → movie-top-rated
GET    /api/movies/recent/       → movie-recent

# Custom Actions (Member)
POST   /api/movies/{pk}/rate/    → movie-rate
GET    /api/movies/{pk}/cast/    → movie-cast

@action(detail=False)

Collection-Action

Wirkt auf Liste

/movies/action_name/

Beispiel: top_rated, recent, stats

@action(detail=True)

Member-Action

Wirkt auf einzelnes Objekt

/movies/{pk}/action_name/

Beispiel: rate, cast, archive

⚙️ @action Parameter

Alle @action Parameter:

from rest_framework.decorators import action

class MovieViewSet(viewsets.ModelViewSet):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer
    
    @action(
        detail=False,                    # Collection (False) oder Member (True)?
        methods=['get', 'post'],         # Erlaubte HTTP-Methoden
        url_path='top-rated',            # Custom URL-Pfad (Standard: Funktionsname)
        url_name='top-rated-movies',     # Custom URL-Name (für reverse)
        permission_classes=[IsAuthenticated],  # Custom Permissions
        serializer_class=TopMovieSerializer,   # Custom Serializer
    )
    def top_rated(self, request):
        """Top-bewertete Filme"""
        # ...
        pass

Beispiele:

class MovieViewSet(viewsets.ModelViewSet):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer
    
    # 1. Standard (GET only)
    @action(detail=False)
    def recent(self, request):
        """URL: /movies/recent/"""
        pass
    
    # 2. Custom URL-Path
    @action(detail=False, url_path='top-10')
    def top_rated(self, request):
        """URL: /movies/top-10/"""
        pass
    
    # 3. Multiple HTTP Methods
    @action(detail=True, methods=['get', 'post', 'delete'])
    def bookmark(self, request, pk=None):
        """
        URL: /movies/{pk}/bookmark/
        GET:    Check if bookmarked
        POST:   Add bookmark
        DELETE: Remove bookmark
        """
        if request.method == 'GET':
            # Check bookmark
            pass
        elif request.method == 'POST':
            # Add bookmark
            pass
        elif request.method == 'DELETE':
            # Remove bookmark
            pass
    
    # 4. Custom Serializer
    @action(
        detail=False,
        serializer_class=MovieStatisticsSerializer
    )
    def statistics(self, request):
        """URL: /movies/statistics/"""
        stats = {
            'total': Movie.objects.count(),
            'avg_rating': Movie.objects.aggregate(Avg('rating'))['rating__avg'],
        }
        serializer = self.get_serializer(stats)
        return Response(serializer.data)
    
    # 5. Custom Permissions
    @action(
        detail=True,
        methods=['post'],
        permission_classes=[IsAdminUser]
    )
    def verify(self, request, pk=None):
        """
        URL: /movies/{pk}/verify/
        Nur für Admins!
        """
        movie = self.get_object()
        movie.is_verified = True
        movie.save()
        return Response({'status': 'verified'})

🎯 Best Practices:

  • ✅ Verwende url_path für kebab-case URLs
  • ✅ Setze permission_classes wenn nötig
  • ✅ Dokumentiere Actions in Docstrings
  • ❌ Nicht zu viele Actions (max. 5-7 pro ViewSet)

🔗 Mehrere Routers kombinieren

Verschiedene Apps, verschiedene Routers

App 1: Movies

# filepath: movies/urls.py
from rest_framework.routers import DefaultRouter
from .views import MovieViewSet, ArtistViewSet

router = DefaultRouter()
router.register(r'movies', MovieViewSet)
router.register(r'artists', ArtistViewSet)

# Kein urlpatterns! Nur router exportieren

App 2: Reviews

# filepath: reviews/urls.py
from rest_framework.routers import DefaultRouter
from .views import ReviewViewSet, CommentViewSet

router = DefaultRouter()
router.register(r'reviews', ReviewViewSet)
router.register(r'comments', CommentViewSet)

Haupt-URLs - Alle kombinieren:

# filepath: movieapi/urls.py
from django.contrib import admin
from django.urls import path, include

# Routers importieren
from movies.urls import router as movies_router
from reviews.urls import router as reviews_router

urlpatterns = [
    path('admin/', admin.site.urls),
    
    # Router URLs einbinden
    path('api/', include(movies_router.urls)),
    path('api/', include(reviews_router.urls)),
    
    # Oder mit Präfix:
    # path('api/movies/', include(movies_router.urls)),
    # path('api/reviews/', include(reviews_router.urls)),
]

📍 Resultierende Endpoints:

GET /api/                  → Root API
GET /api/movies/           → Movies
GET /api/artists/          → Artists
GET /api/reviews/          → Reviews
GET /api/comments/         → Comments

🪆 Nested Routing

Hierarchische URLs

/movies/{id}/castings/ statt /castings/?movie={id}

Problem: Standard-Routing

# Standard (Flach):
GET /api/movies/1/
GET /api/castings/?movie=1  # ← Nicht ideal

# Gewünscht (Nested):
GET /api/movies/1/
GET /api/movies/1/castings/  # ← Besser!

Lösung: drf-nested-routers installieren

# Installation:
pip install drf-nested-routers

ViewSets:

# filepath: movies/views.py
from rest_framework import viewsets
from .models import Movie, MovieCasting
from .serializers import MovieSerializer, MovieCastingSerializer


class MovieViewSet(viewsets.ModelViewSet):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer


class MovieCastingViewSet(viewsets.ModelViewSet):
    serializer_class = MovieCastingSerializer
    
    def get_queryset(self):
        """Nur Castings des Movies"""
        movie_pk = self.kwargs.get('movie_pk')
        return MovieCasting.objects.filter(movie_id=movie_pk)
    
    def perform_create(self, serializer):
        """Movie automatisch setzen"""
        movie_pk = self.kwargs.get('movie_pk')
        serializer.save(movie_id=movie_pk)

Nested Router Setup:

# filepath: movies/urls.py
from rest_framework_nested import routers
from .views import MovieViewSet, MovieCastingViewSet

# Parent Router
router = routers.DefaultRouter()
router.register(r'movies', MovieViewSet, basename='movie')

# Nested Router
movies_router = routers.NestedDefaultRouter(
    router,           # Parent Router
    r'movies',        # Parent Prefix
    lookup='movie'    # Lookup-Name für {movie_pk}
)
movies_router.register(
    r'castings',      # Nested Prefix
    MovieCastingViewSet,
    basename='movie-castings'
)

# URLs kombinieren
urlpatterns = router.urls + movies_router.urls

📍 Resultierende Endpoints:

# Movies (Standard)
GET    /api/movies/
POST   /api/movies/
GET    /api/movies/{id}/
PUT    /api/movies/{id}/
DELETE /api/movies/{id}/

# Castings (Nested)
GET    /api/movies/{movie_pk}/castings/
POST   /api/movies/{movie_pk}/castings/
GET    /api/movies/{movie_pk}/castings/{id}/
PUT    /api/movies/{movie_pk}/castings/{id}/
DELETE /api/movies/{movie_pk}/castings/{id}/

🔢 URL Versioning

API-Versionen über URLs

/api/v1/movies/ vs /api/v2/movies/

1. Versioning in settings.py aktivieren:

# filepath: movieapi/settings.py
REST_FRAMEWORK = {
    'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning',
    'DEFAULT_VERSION': 'v1',
    'ALLOWED_VERSIONS': ['v1', 'v2'],
    'VERSION_PARAM': 'version',
}

2. URLs mit Versionen:

# filepath: movieapi/urls.py
from django.contrib import admin
from django.urls import path, include
from movies.urls import router as movies_router_v1
from movies_v2.urls import router as movies_router_v2

urlpatterns = [
    path('admin/', admin.site.urls),
    
    # API v1
    path('api/v1/', include(movies_router_v1.urls)),
    
    # API v2
    path('api/v2/', include(movies_router_v2.urls)),
]

3. Version in View nutzen:

# filepath: movies/views.py
class MovieViewSet(viewsets.ModelViewSet):
    queryset = Movie.objects.all()
    
    def get_serializer_class(self):
        """Serializer basierend auf Version"""
        if self.request.version == 'v1':
            return MovieSerializerV1
        elif self.request.version == 'v2':
            return MovieSerializerV2
        return MovieSerializer

📍 Endpoints:

GET /api/v1/movies/  → Version 1
GET /api/v2/movies/  → Version 2

Alternative: Query Parameter Versioning

# settings.py:
REST_FRAMEWORK = {
    'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.QueryParameterVersioning',
}

# URLs:
GET /api/movies/?version=v1
GET /api/movies/?version=v2

🎨 Custom URL Patterns mit Router

Router + manuelle URLs kombinieren

Szenario: Router + Custom URLs

# filepath: movies/urls.py
from rest_framework.routers import DefaultRouter
from django.urls import path
from .views import MovieViewSet, MovieSearchAPIView, MovieStatsAPIView

# Router für ViewSet
router = DefaultRouter()
router.register(r'movies', MovieViewSet)

# Router URLs + Custom URLs kombinieren
urlpatterns = [
    # Custom URLs (nicht im ViewSet)
    path('movies/search/', MovieSearchAPIView.as_view(), name='movie-search'),
    path('movies/stats/', MovieStatsAPIView.as_view(), name='movie-stats'),
] + router.urls

# WICHTIG: Custom URLs VOR router.urls!
# Sonst matched /movies/search/ als /movies/{pk}/

📍 Resultierende Endpoints:

# Custom URLs (manuell)
GET /api/movies/search/?q=matrix
GET /api/movies/stats/

# Router URLs (automatisch)
GET /api/movies/
GET /api/movies/{pk}/

Alternative: Alles als @action

class MovieViewSet(viewsets.ModelViewSet):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer
    
    @action(detail=False, methods=['get'])
    def search(self, request):
        """URL: /movies/search/?q=matrix"""
        query = request.query_params.get('q')
        movies = Movie.objects.filter(title__icontains=query)
        serializer = self.get_serializer(movies, many=True)
        return Response(serializer.data)
    
    @action(detail=False, methods=['get'])
    def stats(self, request):
        """URL: /movies/stats/"""
        stats = {
            'total': Movie.objects.count(),
            'avg_rating': Movie.objects.aggregate(Avg('rating'))['rating__avg']
        }
        return Response(stats)

# URLs:
router = DefaultRouter()
router.register(r'movies', MovieViewSet)
urlpatterns = router.urls  # Fertig!

Custom URLs

✅ Separate Views

✅ Mehr Kontrolle

❌ Mehr Code

@action

✅ Alles in einem ViewSet

✅ Weniger Code

✅ Automatische URLs

⭐ Empfohlen!

🔚 Trailing Slashes

Mit oder ohne abschließenden Slash?

Standard: Mit Trailing Slash (Django-Style)

GET /api/movies/      ✅
GET /api/movies       ❌ (Redirect zu /api/movies/)

Trailing Slashes deaktivieren:

# filepath: movies/urls.py
from rest_framework.routers import DefaultRouter

# Trailing Slashes ausschalten
router = DefaultRouter(trailing_slash=False)
router.register(r'movies', MovieViewSet)

urlpatterns = router.urls

# Jetzt:
GET /api/movies  ✅
GET /api/movies/ ❌

Beides erlauben (Optional):

# filepath: movies/urls.py
from rest_framework.routers import DefaultRouter

# Beide Varianten erlauben
router = DefaultRouter()
router.register(r'movies/?', MovieViewSet)  # ← /? macht Slash optional

# Jetzt:
GET /api/movies   ✅
GET /api/movies/  ✅

🎯 Best Practice:

  • Django-Projekte: Mit Trailing Slash (Standard)
  • REST-APIs (allgemein): Ohne Trailing Slash
  • Konsistenz: Wähle EINEN Ansatz für gesamte API!

🧪 URL Testing

URLs testen ist wichtig!

Sicherstellen dass Routing funktioniert

Beispiel: URL Resolution Tests

# filepath: movies/tests/test_urls.py
from django.test import TestCase
from django.urls import reverse, resolve
from rest_framework.test import APITestCase
from movies.views import MovieViewSet


class MovieURLTest(TestCase):
    """Tests für Movie URLs"""
    
    def test_movie_list_url(self):
        """Test: movie-list URL"""
        url = reverse('movie-list')
        self.assertEqual(url, '/api/movies/')
    
    def test_movie_detail_url(self):
        """Test: movie-detail URL"""
        url = reverse('movie-detail', kwargs={'pk': 1})
        self.assertEqual(url, '/api/movies/1/')
    
    def test_movie_top_rated_url(self):
        """Test: Custom Action URL"""
        url = reverse('movie-top-rated')
        self.assertEqual(url, '/api/movies/top_rated/')
    
    def test_movie_rate_url(self):
        """Test: Member Action URL"""
        url = reverse('movie-rate', kwargs={'pk': 1})
        self.assertEqual(url, '/api/movies/1/rate/')
    
    def test_url_resolves_to_view(self):
        """Test: URL löst zu korrektem ViewSet auf"""
        resolver = resolve('/api/movies/')
        self.assertEqual(resolver.func.cls, MovieViewSet)


class MovieAPITest(APITestCase):
    """Tests für Movie API Endpoints"""
    
    def test_movie_list_endpoint(self):
        """Test: GET /api/movies/"""
        response = self.client.get('/api/movies/')
        self.assertEqual(response.status_code, 200)
    
    def test_movie_detail_endpoint(self):
        """Test: GET /api/movies/1/"""
        # Movie erstellen
        movie = Movie.objects.create(
            title="Matrix",
            year=1999,
            genre="Sci-Fi"
        )
        
        response = self.client.get(f'/api/movies/{movie.pk}/')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data['title'], "Matrix")
    
    def test_movie_create_endpoint(self):
        """Test: POST /api/movies/"""
        data = {
            'title': 'Inception',
            'year': 2010,
            'genre': 'Sci-Fi',
            'rating': 8.8
        }
        
        response = self.client.post('/api/movies/', data, format='json')
        self.assertEqual(response.status_code, 201)
        self.assertEqual(Movie.objects.count(), 1)
    
    def test_custom_action_top_rated(self):
        """Test: GET /api/movies/top_rated/"""
        # Test-Daten erstellen
        Movie.objects.create(title="Movie 1", year=2020, rating=9.0)
        Movie.objects.create(title="Movie 2", year=2021, rating=8.5)
        
        response = self.client.get('/api/movies/top_rated/')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.data), 2)
    
    def test_url_not_found(self):
        """Test: Nicht existierende URL"""
        response = self.client.get('/api/movies/99999/')
        self.assertEqual(response.status_code, 404)

💡 Best Practices - URLs & Routing

1. Verwende DefaultRouter

# ✅ EMPFOHLEN:
from rest_framework.routers import DefaultRouter

router = DefaultRouter()
router.register(r'movies', MovieViewSet)
urlpatterns = router.urls
  • ✅ Root API View
  • ✅ Browsable API
  • ✅ Standard für 95% der Fälle

2. Konsistente URL-Struktur

# ✅ GUT (RESTful):
/api/movies/
/api/movies/1/
/api/movies/1/castings/
/api/artists/

# ❌ SCHLECHT (inkonsistent):
/api/getMovies/
/api/movie/1/
/api/getCastings?movieId=1
/api/artist-list/

3. basename richtig setzen

# ✅ Automatisch (wenn queryset vorhanden):
router.register(r'movies', MovieViewSet)

# ✅ Manuell (wenn kein queryset):
router.register(r'movies', MovieViewSet, basename='movie')

# ❌ FEHLER:
router.register(r'movies', MovieViewSet)
# → Error: queryset attribute required!

4. @action statt Custom URLs

# ✅ BESSER (als @action):
class MovieViewSet(viewsets.ModelViewSet):
    @action(detail=False)
    def top_rated(self, request):
        pass

# ❌ SCHLECHTER (Custom URL):
urlpatterns = [
    path('movies/top-rated/', TopRatedAPIView.as_view()),
] + router.urls

5. URL-Namen nutzen

# ✅ GUT (reverse):
from django.urls import reverse
url = reverse('movie-detail', kwargs={'pk': 1})

# ❌ SCHLECHT (hardcoded):
url = '/api/movies/1/'

6. Versioning für Breaking Changes

# ✅ Bei großen Änderungen:
/api/v1/movies/  # Old API
/api/v2/movies/  # New API

# ❌ Nicht für jede kleine Änderung!

7. Nested nur bei Bedarf

# ✅ Nested sinnvoll:
/api/movies/1/castings/

# ❌ Zu tief:
/api/studios/1/movies/1/castings/1/awards/
# → Lieber flach mit Filtern!

8. Trailing Slashes konsistent

# ✅ Django-Standard (mit Slash):
router = DefaultRouter()

# ✅ ODER ohne Slash (konsistent):
router = DefaultRouter(trailing_slash=False)

# ❌ Nicht mischen!

📦 Komplettes Beispiel - Movie API URLs

Production-Ready URL-Konfiguration

1. Models (Recap):

# filepath: movies/models.py
from django.db import models

class Movie(models.Model):
    title = models.CharField(max_length=200, unique=True)
    year = models.IntegerField()
    genre = models.CharField(max_length=100)
    rating = models.DecimalField(max_digits=3, decimal_places=1, null=True, blank=True)
    description = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    
    def __str__(self):
        return f"{self.title} ({self.year})"


class Artist(models.Model):
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    birth_date = models.DateField(null=True, blank=True)
    
    def __str__(self):
        return f"{self.first_name} {self.last_name}"


class MovieCasting(models.Model):
    movie = models.ForeignKey(Movie, on_delete=models.CASCADE, related_name='castings')
    artist = models.ForeignKey(Artist, on_delete=models.CASCADE, related_name='castings')
    role_name = models.CharField(max_length=200)
    is_lead = models.BooleanField(default=False)
    
    def __str__(self):
        return f"{self.artist} as {self.role_name} in {self.movie}"

2. Serializers:

# filepath: movies/serializers.py
from rest_framework import serializers
from .models import Movie, Artist, MovieCasting


class MovieSerializer(serializers.ModelSerializer):
    class Meta:
        model = Movie
        fields = '__all__'
        read_only_fields = ['id', 'created_at', 'updated_at']


class ArtistSerializer(serializers.ModelSerializer):
    class Meta:
        model = Artist
        fields = '__all__'


class MovieCastingSerializer(serializers.ModelSerializer):
    artist_name = serializers.CharField(source='artist.__str__', read_only=True)
    movie_title = serializers.CharField(source='movie.title', read_only=True)
    
    class Meta:
        model = MovieCasting
        fields = '__all__'
        read_only_fields = ['id']

3. ViewSets mit Custom Actions:

# filepath: movies/views.py
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from django.db.models import Avg, Count, Q
from datetime import datetime
from .models import Movie, Artist, MovieCasting
from .serializers import MovieSerializer, ArtistSerializer, MovieCastingSerializer


class MovieViewSet(viewsets.ModelViewSet):
    """
    ViewSet für Movie CRUD + Custom Actions
    """
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer
    
    @action(detail=False, methods=['get'])
    def top_rated(self, request):
        """
        Top 10 Filme nach Rating
        URL: GET /api/movies/top_rated/
        """
        top_movies = Movie.objects.filter(
            rating__isnull=False
        ).order_by('-rating')[:10]
        
        serializer = self.get_serializer(top_movies, many=True)
        return Response(serializer.data)
    
    @action(detail=False, methods=['get'])
    def recent(self, request):
        """
        Filme der letzten 5 Jahre
        URL: GET /api/movies/recent/
        """
        current_year = datetime.now().year
        recent_movies = Movie.objects.filter(
            year__gte=current_year - 5
        ).order_by('-year')
        
        serializer = self.get_serializer(recent_movies, many=True)
        return Response(serializer.data)
    
    @action(detail=False, methods=['get'])
    def statistics(self, request):
        """
        Statistiken über alle Filme
        URL: GET /api/movies/statistics/
        """
        stats = Movie.objects.aggregate(
            total=Count('id'),
            avg_rating=Avg('rating'),
            oldest_year=models.Min('year'),
            newest_year=models.Max('year')
        )
        
        stats['by_genre'] = list(
            Movie.objects.values('genre')
            .annotate(count=Count('id'))
            .order_by('-count')
        )
        
        return Response(stats)
    
    @action(detail=False, methods=['get'])
    def search(self, request):
        """
        Filme suchen
        URL: GET /api/movies/search/?q=matrix&year=1999&genre=Sci-Fi
        """
        queryset = Movie.objects.all()
        
        # Query Parameter auslesen
        query = request.query_params.get('q', None)
        year = request.query_params.get('year', None)
        genre = request.query_params.get('genre', None)
        
        # Filtern
        if query:
            queryset = queryset.filter(
                Q(title__icontains=query) | Q(description__icontains=query)
            )
        
        if year:
            queryset = queryset.filter(year=year)
        
        if genre:
            queryset = queryset.filter(genre__iexact=genre)
        
        serializer = self.get_serializer(queryset, many=True)
        return Response(serializer.data)
    
    @action(detail=True, methods=['post'])
    def rate(self, request, pk=None):
        """
        Film bewerten
        URL: POST /api/movies/{pk}/rate/
        Body: {"rating": 8.5}
        """
        movie = self.get_object()
        rating = request.data.get('rating')
        
        # Validierung
        try:
            rating = float(rating)
            if not (0 <= rating <= 10):
                return Response(
                    {'error': 'Rating must be between 0 and 10'},
                    status=status.HTTP_400_BAD_REQUEST
                )
        except (TypeError, ValueError):
            return Response(
                {'error': 'Invalid rating value'},
                status=status.HTTP_400_BAD_REQUEST
            )
        
        # Speichern
        movie.rating = rating
        movie.save()
        
        serializer = self.get_serializer(movie)
        return Response(serializer.data)
    
    @action(detail=True, methods=['get'])
    def cast(self, request, pk=None):
        """
        Besetzung eines Films
        URL: GET /api/movies/{pk}/cast/
        """
        movie = self.get_object()
        castings = movie.castings.select_related('artist').all()
        
        serializer = MovieCastingSerializer(castings, many=True)
        return Response(serializer.data)


class ArtistViewSet(viewsets.ModelViewSet):
    """
    ViewSet für Artist CRUD + Custom Actions
    """
    queryset = Artist.objects.all()
    serializer_class = ArtistSerializer
    
    @action(detail=True, methods=['get'])
    def movies(self, request, pk=None):
        """
        Alle Filme eines Artists
        URL: GET /api/artists/{pk}/movies/
        """
        artist = self.get_object()
        castings = artist.castings.select_related('movie').all()
        
        # Unique Movies extrahieren
        movies = list(set([casting.movie for casting in castings]))
        
        serializer = MovieSerializer(movies, many=True)
        return Response(serializer.data)


class MovieCastingViewSet(viewsets.ModelViewSet):
    """
    ViewSet für MovieCasting CRUD
    """
    queryset = MovieCasting.objects.select_related('movie', 'artist').all()
    serializer_class = MovieCastingSerializer
    
    @action(detail=False, methods=['get'])
    def lead_roles(self, request):
        """
        Alle Hauptrollen
        URL: GET /api/castings/lead_roles/
        """
        lead_castings = MovieCasting.objects.filter(
            is_lead=True
        ).select_related('movie', 'artist')
        
        serializer = self.get_serializer(lead_castings, many=True)
        return Response(serializer.data)

🔗 Komplettes Beispiel - URLs Setup

4. App URLs - Router Setup:

# filepath: movies/urls.py
from rest_framework.routers import DefaultRouter
from .views import MovieViewSet, ArtistViewSet, MovieCastingViewSet

# Router erstellen
router = DefaultRouter()

# ViewSets registrieren
router.register(r'movies', MovieViewSet, basename='movie')
router.register(r'artists', ArtistViewSet, basename='artist')
router.register(r'castings', MovieCastingViewSet, basename='casting')

# URLs exportieren
urlpatterns = router.urls

5. Haupt-URLs - Integration:

# filepath: movieapi/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    # Django Admin
    path('admin/', admin.site.urls),
    
    # API URLs
    path('api/', include('movies.urls')),
    
    # DRF Auth (Login/Logout für Browsable API)
    path('api-auth/', include('rest_framework.urls')),
]

6. Settings - DRF Konfiguration:

# filepath: movieapi/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    
    # DRF
    'rest_framework',
    
    # Apps
    'movies',
]

REST_FRAMEWORK = {
    # Pagination
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 10,
    
    # Filtering
    'DEFAULT_FILTER_BACKENDS': [
        'rest_framework.filters.OrderingFilter',
        'rest_framework.filters.SearchFilter',
    ],
    
    # Permissions (optional)
    # 'DEFAULT_PERMISSION_CLASSES': [
    #     'rest_framework.permissions.IsAuthenticatedOrReadOnly',
    # ],
}

📍 Komplettes Beispiel - Alle Endpoints

🌐 Resultierende API-Struktur:

# Root API
GET    /api/                              → API Root (Übersicht)

# Movies - CRUD
GET    /api/movies/                       → Liste aller Filme (paginiert)
POST   /api/movies/                       → Film erstellen
GET    /api/movies/{id}/                  → Film-Details
PUT    /api/movies/{id}/                  → Film komplett aktualisieren
PATCH  /api/movies/{id}/                  → Film teilweise aktualisieren
DELETE /api/movies/{id}/                  → Film löschen

# Movies - Custom Actions (Collection)
GET    /api/movies/top_rated/             → Top 10 Filme
GET    /api/movies/recent/                → Filme der letzten 5 Jahre
GET    /api/movies/statistics/            → Statistiken
GET    /api/movies/search/?q=matrix       → Filme suchen

# Movies - Custom Actions (Member)
POST   /api/movies/{id}/rate/             → Film bewerten
GET    /api/movies/{id}/cast/             → Besetzung anzeigen

# Artists - CRUD
GET    /api/artists/                      → Liste aller Artists
POST   /api/artists/                      → Artist erstellen
GET    /api/artists/{id}/                 → Artist-Details
PUT    /api/artists/{id}/                 → Artist aktualisieren
PATCH  /api/artists/{id}/                 → Artist teilweise aktualisieren
DELETE /api/artists/{id}/                 → Artist löschen

# Artists - Custom Actions
GET    /api/artists/{id}/movies/          → Filme eines Artists

# Castings - CRUD
GET    /api/castings/                     → Liste aller Castings
POST   /api/castings/                     → Casting erstellen
GET    /api/castings/{id}/                → Casting-Details
PUT    /api/castings/{id}/                → Casting aktualisieren
PATCH  /api/castings/{id}/                → Casting teilweise aktualisieren
DELETE /api/castings/{id}/                → Casting löschen

# Castings - Custom Actions
GET    /api/castings/lead_roles/          → Alle Hauptrollen

# Authentication (Browsable API)
GET    /api-auth/login/                   → Login
GET    /api-auth/logout/                  → Logout

🐛 URL-Debugging

Wie finde ich heraus welche URLs existieren?

1. Django Shell - Alle URLs anzeigen:

python manage.py shell

>>> from django.urls import get_resolver
>>> resolver = get_resolver()
>>> for pattern in resolver.url_patterns:
...     print(pattern)

# Oder spezifischer:
>>> from rest_framework.routers import DefaultRouter
>>> from movies.urls import router
>>> for url in router.urls:
...     print(url.pattern)

2. Management Command - URLs ausgeben:

# Django 3.0+:
python manage.py show_urls

# Output:
/api/                              → api-root
/api/movies/                       → movie-list
/api/movies/{pk}/                  → movie-detail
/api/movies/top_rated/             → movie-top-rated
...

3. Browsable API - Root View:

# Browser öffnen:
http://localhost:8000/api/

# DefaultRouter zeigt automatisch alle Endpoints!

4. Custom Management Command erstellen:

# filepath: movies/management/commands/list_urls.py
from django.core.management.base import BaseCommand
from django.urls import get_resolver


class Command(BaseCommand):
    help = 'Liste alle URLs'
    
    def handle(self, *args, **options):
        resolver = get_resolver()
        
        def show_urls(urllist, depth=0):
            for entry in urllist:
                print("  " * depth + str(entry.pattern))
                if hasattr(entry, 'url_patterns'):
                    show_urls(entry.url_patterns, depth + 1)
        
        show_urls(resolver.url_patterns)

# Verwendung:
python manage.py list_urls

5. django-extensions installieren:

# Installation:
pip install django-extensions

# settings.py:
INSTALLED_APPS = [
    # ...
    'django_extensions',
]

# Alle URLs anzeigen:
python manage.py show_urls

# Mit Details:
python manage.py show_urls --format table

⚡ URL-Performance

URLs effizient konfigurieren

❌ Schlecht: Komplexe RegEx

from django.urls import re_path

urlpatterns = [
    re_path(
        r'^api/movies/(?P[0-9]{4})/(?P[0-9]{2})/(?P[0-9]{2})/$',
        views.movies_by_date
    ),
]

# ← Langsam! RegEx muss bei jedem Request gematched werden

✅ Besser: Einfache path()

from django.urls import path

urlpatterns = [
    path('api/movies/', views.movie_list),
    path('api/movies//', views.movie_detail),
]

# ← Schnell! Django cached URL-Patterns

❌ Schlecht: Viele Custom URLs

urlpatterns = [
    path('movies/', MovieListView.as_view()),
    path('movies//', MovieDetailView.as_view()),
    path('movies/top/', TopMoviesView.as_view()),
    path('movies/recent/', RecentMoviesView.as_view()),
    path('movies/search/', SearchView.as_view()),
    path('movies//rate/', RateView.as_view()),
    path('movies//cast/', CastView.as_view()),
    # ... 20 mehr URLs
]

# ← Viele Views, viele URLs = mehr Code

✅ Besser: ViewSet + Router

class MovieViewSet(viewsets.ModelViewSet):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer
    
    @action(detail=False)
    def top(self, request): pass
    
    @action(detail=False)
    def recent(self, request): pass
    
    @action(detail=False)
    def search(self, request): pass
    
    @action(detail=True)
    def rate(self, request, pk): pass
    
    @action(detail=True)
    def cast(self, request, pk): pass

router = DefaultRouter()
router.register(r'movies', MovieViewSet)

# ← 1 ViewSet, automatische URLs!

🎯 Performance-Tipps:

  • path() bevorzugen über re_path()
  • ✅ Router für ViewSets nutzen (weniger URL-Patterns)
  • ✅ URL-Patterns oben sind schneller (werden zuerst gematcht)
  • include() für App-URLs (bessere Organisation)

⚠️ Häufige Fehler bei URLs

❌ Fehler 1: basename vergessen

# FEHLER:
class MovieViewSet(viewsets.ModelViewSet):
    serializer_class = MovieSerializer
    
    def get_queryset(self):
        return Movie.objects.all()
    # Kein queryset-Attribut!

router.register(r'movies', MovieViewSet)
# ← Error: basename required!

# FIX:
router.register(r'movies', MovieViewSet, basename='movie')

❌ Fehler 2: URL-Reihenfolge falsch

# FEHLER:
urlpatterns = [
    path('movies//', movie_detail),  # ← Matched zuerst!
    path('movies/top/', top_movies),         # ← Wird nie erreicht!
]
# /movies/top/ wird als /movies// interpretiert!

# FIX:
urlpatterns = [
    path('movies/top/', top_movies),         # ← Spezifisch zuerst!
    path('movies//', movie_detail),  # ← Generisch zuletzt
]

❌ Fehler 3: Trailing Slash inkonsistent

# FEHLER:
urlpatterns = [
    path('movies/', movie_list),      # Mit Slash
    path('artists', artist_list),     # Ohne Slash
]

# FIX:
urlpatterns = [
    path('movies/', movie_list),
    path('artists/', artist_list),    # Konsistent!
]

❌ Fehler 4: Router URLs falsch kombiniert

# FEHLER:
urlpatterns = router.urls + [
    path('custom/', custom_view),
]
# Custom URLs werden NACH Router-URLs gematcht!

# FIX:
urlpatterns = [
    path('custom/', custom_view),  # Spezifisch zuerst
] + router.urls

❌ Fehler 5: include() vergessen

# FEHLER (movieapi/urls.py):
from movies.views import MovieViewSet

urlpatterns = [
    path('api/movies/', MovieViewSet.as_view()),  # ← Falsch!
]

# FIX:
from django.urls import include

urlpatterns = [
    path('api/', include('movies.urls')),  # ← Korrekt!
]

❌ Fehler 6: .as_view() vergessen

# FEHLER:
urlpatterns = [
    path('movies/', MovieListView),  # ← Klasse statt Instance!
]

# FIX:
urlpatterns = [
    path('movies/', MovieListView.as_view()),  # ← Korrekt!
]

🎯 Zusammenfassung

Was haben wir gelernt?

📜 URL Basics

  • ✅ Django URL-Patterns Recap
  • ✅ path() vs re_path()
  • ✅ URL-Parameter & Converter

🔧 Manuelle URLs

  • ✅ APIView URLs
  • ✅ Generic Views URLs
  • ✅ URL-Namen für reverse()

⭐ Routers

  • ✅ SimpleRouter vs DefaultRouter
  • ✅ router.register()
  • ✅ basename Parameter
  • ✅ Custom Actions (@action)

🎬 Custom Actions

  • ✅ @action(detail=False) - Collection
  • ✅ @action(detail=True) - Member
  • ✅ url_path, url_name, methods
  • ✅ Custom Permissions/Serializers

🪆 Advanced

  • ✅ Nested Routing (drf-nested-routers)
  • ✅ URL Versioning
  • ✅ Mehrere Routers kombinieren
  • ✅ Trailing Slashes

💡 Best Practices

  • ✅ DefaultRouter verwenden
  • ✅ ViewSets statt manuelle URLs
  • ✅ @action für Custom Endpoints
  • ✅ Konsistente URL-Struktur
  • ✅ URL-Namen nutzen (reverse)

📊 Vergleich - Alle URL-Ansätze

❌ 1. Manuelle URLs (APIView)

~15 URL-Patterns

urlpatterns = [
    # CRUD
    path('movies/', MovieListAPIView.as_view()),
    path('movies//', MovieDetailAPIView.as_view()),
    
    # Custom
    path('movies/top/', TopMoviesView.as_view()),
    path('movies/recent/', RecentMoviesView.as_view()),
    path('movies/search/', SearchView.as_view()),
    path('movies//rate/', RateView.as_view()),
    path('movies//cast/', CastView.as_view()),
    
    # Artists
    path('artists/', ArtistListAPIView.as_view()),
    path('artists//', ArtistDetailAPIView.as_view()),
    path('artists//movies/', ArtistMoviesView.as_view()),
    
    # Castings
    path('castings/', CastingListAPIView.as_view()),
    path('castings//', CastingDetailAPIView.as_view()),
    path('castings/lead/', LeadRolesView.as_view()),
]

✅ Vorteile: Maximale Kontrolle

❌ Nachteile: Viel Code, fehleranfällig

📝 2. Manuelle URLs (Generic Views)

~10 URL-Patterns

urlpatterns = [
    # CRUD (weniger Views)
    path('movies/', MovieListCreateView.as_view()),
    path('movies//', MovieDetailView.as_view()),
    
    # Custom (noch manuell)
    path('movies/top/', TopMoviesView.as_view()),
    path('movies/recent/', RecentMoviesView.as_view()),
    path('movies/search/', SearchView.as_view()),
    
    # Artists
    path('artists/', ArtistListCreateView.as_view()),
    path('artists//', ArtistDetailView.as_view()),
    
    # Castings
    path('castings/', CastingListCreateView.as_view()),
    path('castings//', CastingDetailView.as_view()),
]

✅ Vorteile: Weniger Code als APIView

❌ Nachteile: URLs noch manuell

✅ 3. Router (ViewSets)

~5 Zeilen Code!

# ALLE URLs automatisch!
router = DefaultRouter()
router.register(r'movies', MovieViewSet)
router.register(r'artists', ArtistViewSet)
router.register(r'castings', MovieCastingViewSet)

urlpatterns = router.urls

# Custom Actions im ViewSet:
class MovieViewSet(viewsets.ModelViewSet):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer
    
    @action(detail=False)
    def top(self, request): pass
    
    @action(detail=False)
    def recent(self, request): pass
    
    @action(detail=False)
    def search(self, request): pass
    
    @action(detail=True)
    def rate(self, request, pk): pass
    
    @action(detail=True)
    def cast(self, request, pk): pass

✅ Vorteile: Minimal Code, automatisch, RESTful

⭐ Production-Ready!

📝 Cheat Sheet - DRF URLs & Routing

🔧 Router Setup

# DefaultRouter (Empfohlen!)
from rest_framework.routers import DefaultRouter

router = DefaultRouter()
router.register(r'movies', MovieViewSet)
urlpatterns = router.urls

# Mit basename (bei get_queryset())
router.register(r'movies', MovieViewSet, basename='movie')

# Ohne Trailing Slash
router = DefaultRouter(trailing_slash=False)

🎬 Custom Actions

# Collection Action
@action(detail=False, methods=['get'])
def top_rated(self, request):
    pass
# URL: /movies/top_rated/

# Member Action
@action(detail=True, methods=['post'])
def rate(self, request, pk):
    pass
# URL: /movies/{pk}/rate/

# Custom URL-Path
@action(detail=False, url_path='top-10')
def top_rated(self, request):
    pass
# URL: /movies/top-10/

🔗 URL-Namen verwenden

# reverse() in Code
from django.urls import reverse
url = reverse('movie-list')
url = reverse('movie-detail', kwargs={'pk': 1})
url = reverse('movie-top-rated')

# In Templates
{% url 'movie-list' %}
{% url 'movie-detail' pk=movie.id %}

🪆 Nested Routing

# Installation
pip install drf-nested-routers

# Setup
from rest_framework_nested import routers

router = routers.DefaultRouter()
router.register(r'movies', MovieViewSet)

movies_router = routers.NestedDefaultRouter(
    router, r'movies', lookup='movie'
)
movies_router.register(
    r'castings', MovieCastingViewSet
)

urlpatterns = router.urls + movies_router.urls

🔢 Versioning

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_VERSIONING_CLASS': 
        'rest_framework.versioning.URLPathVersioning',
}

# urls.py
urlpatterns = [
    path('api/v1/', include(router_v1.urls)),
    path('api/v2/', include(router_v2.urls)),
]

📍 URL-Parameter

# path() Converter
      # Integer
    # String
     # UUID
   # Pfad (mehrere Segmente)

# Beispiele
path('movies//', ...)
path('movies//', ...)
path('files//', ...)

🚀 Nächste Schritte

1

Authentication & Permissions

JWT Tokens

Session Auth

Custom Permissions

2

Filtering & Pagination

django-filter

Custom Filters

Pagination Classes

3

Testing

APITestCase

URL Tests

Integration Tests

4

Documentation

drf-spectacular

Swagger/OpenAPI

API Docs

5

Deployment

Production Settings

Docker

CORS & Security

📚 Ressourcen & Weiterführendes

📖 Offizielle Dokumentation

  • DRF Routers:
    https://www.django-rest-framework.org/api-guide/routers/
  • ViewSets:
    https://www.django-rest-framework.org/api-guide/viewsets/
  • Django URLs:
    https://docs.djangoproject.com/en/stable/topics/http/urls/

🛠️ Packages

  • drf-nested-routers:
    Nested Routing für DRF
    pip install drf-nested-routers
  • django-extensions:
    show_urls Command
    pip install django-extensions

📝 Tools

  • Browsable API: http://localhost:8000/api/
  • Django Admin: http://localhost:8000/admin/
  • show_urls: python manage.py show_urls

🎉 Gratulation!

Du beherrschst jetzt DRF URLs & Routing!

✅ Was du jetzt kannst:

  • 🔧 Django URL-Basics verstehen
  • 📝 Manuelle URLs konfigurieren
  • ⭐ Router für ViewSets nutzen
  • 🎬 Custom Actions erstellen
  • 🪆 Nested Routing implementieren
  • 🔢 API-Versioning einrichten
  • 🐛 URLs debuggen
  • ⚡ Performance optimieren

🎯 Best Practices gelernt:

  • ✅ DefaultRouter verwenden
  • ✅ ViewSets statt manuelle URLs
  • ✅ @action für Custom Endpoints
  • ✅ basename richtig setzen
  • ✅ URL-Namen für reverse()
  • ✅ Konsistente Struktur

🚀 Nächster Schritt:

Implementiere eigene API mit automatischem Routing!

  • Erstelle ViewSets
  • Nutze DefaultRouter
  • Füge Custom Actions hinzu
  • Teste alle Endpoints

Viel Erfolg mit deinen APIs! 🚀

Keep coding, keep learning! 💻

1 / 32